Adopt Xcode String Catalog symbol generation across the app#79
Conversation
Turn on Xcode's type-safe String Catalog codegen across the six catalog-backed targets, the first step of the codegen migration. - Project.swift: set STRING_CATALOG_GENERATE_SYMBOLS=YES in the project base settings so the Tuist-native app/extension targets generate LocalizedStringResource symbols. The SwiftPM package targets (WhereUI, WhereCore, RegionKit, LifecycleKit) generate symbols automatically from the toolchain and need no setting. - Normalize the two Tuist-native catalogs so every key produces a symbol: WhereWidgets keys stale -> manual; WhereShareExtension keys gain extractionState: manual. Verified by building RegionKit (SPM) and WhereWidgets (Tuist-native) against a throwaway generated-symbol reference; both compiled. No behavior change.
Region.localizedName now switches over the generated .region<Case> symbols (String(localized: .regionCalifornia)) instead of raw "region.<rawValue>" keys with bundle: .module. The exhaustive switch still makes a new region case a compile error until its manual catalog key (and thus symbol) exists; update the README's add-a-region note accordingly.
Replace the String(localized:bundle:.module) title/retry lookups with the generated .failureLaunchTitle / .failureLaunchRetry symbols passed straight to Label and Button.
Replace every String(localized: "key", bundle: .module) in the reminder,
daily-summary, issue-alert, and backup paths with the generated
LocalizedStringResource symbols. Plural/interpolated keys become generated
functions (.dataIssuesNotificationBody(count), .summaryNotificationDayCount(days),
.backupErrorUnsupportedFormatVersion(version)).
DailySummaryReconciler.summaryFragment and BackupError.errorDescription drop
their String(format:) composition in favor of the type-safe symbols; the
composed summary body ("3 days in California, 1 day in New York") is unchanged,
verified by WhereCoreTests.
Delete the ~1730-line hand-written Strings enum and reference Xcode's generated LocalizedStringResource symbols directly at call sites (Text(.tabPrimary), String(localized: .commonOk), etc.). ~277 call sites across 30 view files. - Add WhereFormat: the small set of helpers that genuinely need logic — the non-catalog formatters (grouping-free year, drift measurement, coordinate), count/plural helpers, enum switches (region-map kind, resolution section, recent-activity window/reason), and the composed accessibility labels. Each composes generated symbols with runtime values, so a removed key is a compile error. - EvidenceKind.displayName now switches over the generated evidence-kind symbols. - Catalog: mark the 46 auto-extracted keys manual and add the 34 Evidence/common keys that only existed via defaultValue: so every referenced key now generates a symbol; the file is re-sorted with all keys manual. - Replace StringsTests with WhereFormatTests: compile-time safety covers key existence, so the suite now pins catalog values, plural variations, year formatting, EvidenceKind names, and the accessibility-cue composition. All 198 WhereUITests pass; Where app builds.
Replace the ShareStrings facade with the extension's own generated LocalizedStringResource symbols in ShareEvidenceView (bare symbols in Text/Button, String(localized:) elsewhere; the attachment header picks its plural symbol inline). Delete ShareStrings.swift.
Replace the WidgetStrings facade with the extension's generated symbols for the widget-gallery name/description on each StaticConfiguration (String(localized: .widgetGalleryTodayName), etc.). Delete WidgetStrings.swift.
Update the localization guidance to the generated-symbol model: add manual keys to the catalog and reference the generated LocalizedStringResource symbols directly (Text(.tabPrimary), String(localized: .commonOk)), with WhereUI's composition/formatting living in WhereFormat. Drops the Strings.swift / ShareStrings / WidgetStrings / bundle: .module references from the Where feature AGENTS.md and the WhereCore / WhereWidgets / WhereShareExtension module docs.
CI is red due to an Xcode-toolchain gap, not a defect in this change — recommend holding until the runners have Xcode 27. Marking as draft. Root causeXcode's built-in String Catalog symbol generation for SwiftPM package targets is unreliable on Xcode 26.x. Our Reproduced locally under Xcode 26.6 (Swift 6.3.3, = the Fixes explored — all dead ends on 26.x
Corroboration that this area is buggy / actively patched on 26.x
RecommendationThe branch is correct and needs zero changes — it's green on Xcode 27. Hold as draft until the CI runners have Xcode 27, at which point this should pass as-is. If we need type-safe strings sooner, the only 26.x-viable path is a custom SwiftPM build-tool-plugin codegen (gated to not run under Xcode) — more moving parts; happy to spike it separately if desired. |
What
Switches the whole repo onto Xcode's built-in String Catalog symbol generation and makes every string reference type-safe. The hand-maintained string facades (
Strings.swift~1,730 lines,ShareStrings,WidgetStrings) and all rawString(localized: "key", bundle: .module)lookups are gone; user-facing copy now resolves through generatedLocalizedStringResourcesymbols, so a typo'd or removed key is a compile error instead of a runtime surprise.Scope is the codegen migration only — no new localization of currently-hardcoded/DEBUG surfaces or Info.plist strings.
How it works
STRING_CATALOG_GENERATE_SYMBOLS = YESis set project-wide inProject.swiftfor the Tuist-native app/extension targets. The SwiftPM library targets generate symbols automatically from the toolchain (verified empirically), so they need no wiring.some.keygenerates a.someKeysymbol onLocalizedStringResource, referenced directly at call sites:Text(.tabPrimary),String(localized: .commonOk),Label(.evidenceAdd, systemImage:).internalper target — fine here, because cross-module reuse already flows through public Swift API (e.g. the share extension renders kinds via WhereUI's publicEvidenceKind.displayName), never raw keys.Commits (one per module, bisectable)
manual.Region.localizedNameswitches over.regionCaliforniaetc.LifecycleFailureViewuses.failureLaunchTitle/.failureLaunchRetry.String(format:)compositions replaced with type-safe symbols (output unchanged).Stringsfacade; rewrite ~277 call sites across 30 files. AddWhereFormatfor the cases that genuinely need logic (grouping-free years, plurals, enum switches, composed accessibility labels), each composing generated symbols internally. Add the 34 Evidence/common keys that previously only existed viadefaultValue:. ReplaceStringsTestswithWhereFormatTests.ShareStrings/WidgetStringsfor the extensions' own generated symbols.Verification
./swiftformat --lint .clean (326 files).Stuff-iOS-Testsscheme green (all bundles) on iPhone 17 / iOS 26.2.defaultValue:separators and argument order, and no catalog value contains markdown, soText(String)→Text(.symbol)renders identically.Reviewer notes
manualcatalog — it's a re-sort +extractionStatenormalization, not content edits.Text/Label/Button(verified to acceptLocalizedStringResource), call sites useString(localized: .symbol)as a deliberate safety choice; a follow-up could tighten.navigationTitle(...)etc. to bare symbols.WhereFormat's enum-switch/compose helpers were cross-checked by hand against the old mappings; the untested cases are at parity with the previousStringsTestscoverage.